2013年12月12日 星期四

iOS App 串接 Dropbox API第一次就上手

前言
最近發現我好飢渴阿(大誤!)
很多網路服務API都很有趣, 也很完整, 高手滿坑滿谷.
就像Steve Jobs說的 'stay hungry stay foolish'
今天就來玩玩Dropbox的API吧~


申請APP帳號
Dropbox開發者首頁(Link)
左側有App Console的標籤, 點擊然後選擇Create app.

> 要先同意使用者條款跟隱私權政策


> 然後要選擇app的類型.
1. 先選右邊的Dropbox API app
2. 然後選擇Files and datastores (注意!選Datastores only沒法對data做access動作!)
    就像這位仁兄遇到的問題一樣.
3. 我只允許我的app access自己app所建立的資料夾
4. 設定一下app名稱 (之後還可以更改)


> 建立完成, 複製App key以及App secret等等要用.


> 一切就緒, 讓我們切換到Core API的標籤頁, 點選Install SDK


先從Example Project試試
剛剛下載的iOS SDK裡面有個examples > DBRoulette 開啟DBRoulette.xcodeproj
把剛剛的App key跟App secret 貼到 DBRouletteAppDelegate.m
注意!root = kDBRootAppFolder/kDBRootDropbox 不要用預設的nil
不然跑起來會有錯誤訊息:
[WARNING] DropboxSDK: error making request to /1/metadata/(null) - (400) Expected 'root' ...

然後DBRoulette-Info.plist右鍵點選Open as > Source code
注意!db-(your app key) 記得保留'db-' .

設定完成來執行看看吧!

一開始可能會有一堆錯誤視窗跑出來.
因為資料夾是空的呀~不過登入自己的Dropbox可以發現.

Dropbox有建立了一個新的資料夾了, 丟一些照片進去就大功告成了!
下台一鞠躬, 謝謝大家XD
Sent from Evernote

2013年12月11日 星期三

iOS App share with Google Plus

前言
Google Plus的分享功能要加到iOS專案裡面不會太困難.
官方網站的原文教學

環境設定
下載Google+ iOS SDK (official link)
檢查一下
AssetsLibrary.framework
Foundation.framework
CoreLocation.framework
CoreMotion.framework
CoreGraphics.framework
CoreText.framework
MediaPlayer.framework
Security.framework
SystemConfiguration.framework
UIKit.framework

並從下載回來的Google+ iOS SDK資料夾內
拖曳&import
GooglePlus.framework
GoogleOpenSource.framework



建立一個API project
舊版的設定頁面


註冊一個app


完成後應該長得像這樣


新版的設定頁面
啟用Google+API


註冊一個app


填寫必要資訊


完成後應該長得像這樣



程式
在AppDelegate.m

#import 
#import  
static NSString * const kClientID = @"blahblahblah.apps.googleusercontent.com";

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Set app's client ID for |GPPSignIn| and |GPPShare|.
    [GPPSignIn sharedInstance].clientID = kClientID;
    ...
}

在ViewController.m
@interface YourViewController ()  {
     ...
- (void)viewDidLoad {
     [super viewDidLoad];
     [GPPShare sharedInstance].delegate = self;
}
     ...
- (void)gplusBtnPressed:(id)sender {   
    id shareBuilder = [[GPPShare sharedInstance] shareDialog];
   
    // This line will fill out the title, description, and thumbnail of the item
    // you're sharing based on the URL you included.
    //[shareBuilder setURLToShare:[NSURL URLWithString:@"The url you want to share"]];
    [shareBuilder setContentDeepLinkID:@"DeepLinkID"];
    [shareBuilder setTitle:@" 標題 "
               description:@" 描述"]
              thumbnailURL:[NSURL URLWithString:[@"縮圖網址"]]];

    [shareBuilder setPrefillText:msg];
    [shareBuilder open];
}

#pragma mark - GPPShareDelegate
- (void)finishedSharing:(BOOL)shared {
   
}

- (void)reportAuthStatus {
    if ([GPPSignIn sharedInstance].authentication) {
        NSLog(@"Status: Authenticated");
    } else {
        // To authenticate, use Google+ sign-in button.
        NSLog(@"Status: Not authenticated");
    }
}

如果分享時發生Error 404 Not Found, 那最有可能就是client ID沒有符合

希望大家都分享順利囉~
Sent from Evernote

[Android] share with Facebook SDK 3.5

前言
原本在開發者沙盒(SandBox)模式下一切都還滿順利的,
不料上到Google Play發現原本的功能不work, 
花了一些時間才發現小細節.
自己記錄一下, 同時也給需要的人參考一下.

以下我就自己的開發歷程, 
先從沙盒模式說起, 
最後再談關閉沙盒模式要注意的地方.


下載&Import FaceBook SDK
可以參考官方文件的原文教學
我是用SDK 3.5.2 (Official Download Link / Github SDK3.6)

I'll skip the installation of Facebook in emulator here.

> Right click / 'File' on top

> Existing Android Code Into Workspace

> make sure the 'facebook' project is checked

如果Project有Error可以先檢查android-support-v4.jar的版本是否一致
最快的方法就是把最新的jar在project list裡面copy起來,
然後把其他project有用到的都先delete掉, 再paste上去.


環境設定
接下來看看我們的project要做哪些設定
1. library

2. Manifest設定
     
     
        
    

3. res/values/strings.xml加上
(your Facebook App ID)


Facebook上建立應用程式後, copy App ID

4. 在Facebook App設定頁面加上Hash Key
Mac User
keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore | openssl sha1 -binary | openssl base64



程式 - Facebook分享
// facebook
private static final String PERMISSION = "publish_actions";
private UiLifecycleHelper uiHelper;
private boolean canPresentShareDialog;
private PendingAction pendingAction = PendingAction.NONE;
private enum PendingAction {
     NONE,
    POST_PHOTO,
    POST_STATUS_UPDATE
}

// Facebook Login
Session.openActiveSession(this, true, new Session.StatusCallback()
{
     // callback when session changes state
     @Override
     public void call(final Session session, SessionState state, Exception exception)
     {
          if (session.isOpened())
          {   
               // make request to the /me API
               Request request = Request.newMeRequest(session, new Request.GraphUserCallback()
               {
                    @Override
                    public void onCompleted(GraphUser user, Response response)
                    {
                         // If the response is successful
                        Log.d("facebook", "GraphUserCallback" + user.getId()+" " + response.toString());
                                        
                        if (session == Session.getActiveSession())
                        {
                              if (user != null)
                             {
                                   performPublish(PendingAction.POST_STATUS_UPDATE, canPresentShareDialog);
                             }
                        }
                                        
                        if (response.getError() != null)
                        {
                              // Handle errors, will do so later.
                        }
                     }
               });
               request.executeAsync();
          }
     }
});

private void performPublish(PendingAction action, boolean allowNoSession) {
     Session session = Session.getActiveSession();
    if (session != null) {
        pendingAction = action;
        if (hasPublishPermission()) {
               // We can do the action right away.
            handlePendingAction();
            return;
        } else if (session.isOpened()) {
            // We need to get new permissions, then complete the action when we get called back.
            session.requestNewPublishPermissions(new Session.NewPermissionsRequest(this, PERMISSION));
            return;
          }
    }

    if (allowNoSession) {
          pendingAction = action;
        handlePendingAction();
     }
}
    
@SuppressWarnings("incomplete-switch")
private void handlePendingAction() {
    PendingAction previouslyPendingAction = pendingAction;
    // These actions may re-set pendingAction if they are still pending, but we assume they
    // will succeed.
    pendingAction = PendingAction.NONE;

    switch (previouslyPendingAction) {
          case POST_PHOTO:
               //postPhoto();
            break;
        case POST_STATUS_UPDATE:
            postStatusUpdate();
            break;
    }
}
    
private FacebookDialog.ShareDialogBuilder createShareDialogBuilder() {
    return new FacebookDialog.ShareDialogBuilder(this)
          .setName("fb app name")
        .setDescription("app description")
        .setLink("site link");
}
    
private void postStatusUpdate() {
     if (canPresentShareDialog) {
        FacebookDialog shareDialog = createShareDialogBuilder().build();
        uiHelper.trackPendingDialogCall(shareDialog.present());
    } else if (hasPublishPermission()) {
        Bundle params = new Bundle();
        params.putString("name", " link主標題 ");
        params.putString("caption", " link副標題 ");
        params.putString("message", " 描述 ");
        params.putString("link", " 分享連結 ");
        params.putString("picture", " 圖片url ");

        Request request = new Request(Session.getActiveSession(), "me/feed", params, HttpMethod.POST);
        request.setCallback(new Request.Callback() {
               @Override
             public void onCompleted(Response response) {
                    showPublishResult(null, response.getGraphObject(), response.getError());
             }
        });
        request.executeAsync();
     } else {
          pendingAction = PendingAction.POST_STATUS_UPDATE;
    }
}
    
private void showPublishResult(String message, GraphObject result, FacebookRequestError error) {
     String title = null;
    String alertMessage = null;
    if (error == null) {
          title = getString(R.string.success);
        alertMessage = getString(R.string.successfully_posted_post);
    } else {
        title = getString(R.string.error);
        alertMessage = error.getErrorMessage();
    }

    new AlertDialog.Builder(this)
          .setTitle(title)
        .setMessage(alertMessage)
        .setPositiveButton(R.string.ok, null)
        .show();
}

@Override
protected void onActivityResult(int requestCode, int responseCode, Intent intent) {  
    // fb 登入結果
    Session.getActiveSession().onActivityResult(this, requestCode, responseCode, intent);
}


正式版本
1. Facebook APP設定頁面一定要把SandBox mode關閉

2. 確認Android app package name
> Export Signed Application Package

> KeyStore path別忘記副檔名.keystore !

> 輸入key的資訊 (一個keystore可以有很多把keys), Alias就是key的名稱

3. 複製正式版本的hashed key (建議在keystore的目錄下)
keytool -exportcert -alias (key的名稱) -keystore (keystore名稱).keystore | openssl sha1 -binary | openssl base64

這樣submit的正式版app就能夠正常分享資訊到Facebook啦!

Sent from Evernote

2013年12月4日 星期三

在iOS app用webview播放YouTube影片

前言
如果要直接launch YouTube app來播放, 那需要URL scheme:
  • youtube://
  • http://www.youtube.com/v/VIDEO_IDENTIFIER
  • http://www.youtube.com/watch?v=VIDEO_IDENTIFIER
今天我需要用嵌入webview 方式播放YouTube影片來提供比較好的使用者體驗:D



實作
原理就是用webview 的loadHTMLString 方法來讀取我們assign好的HTML內容.
廢話不說, 直接上code!
webplayer = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)];
    webplayer.scalesPageToFit = YES;
    webplayer.delegate = self;
    [self.view addSubview:webplayer];
    NSString *videoURL = [NSString stringWithFormat:@"http://www.youtube.com/embed/%@", youtubeID];
    NSString *videoHTML = [NSString stringWithFormat:@"\
                 \
                 \
                 \
                 \
                 \
                 \
                 \
                 ", videoURL];
    [webplayer loadHTMLString:videoHTML baseURL:nil];
    webplayer.backgroundColor = [UIColor blackColor];
    webplayer.opaque = NO;



重點一
如果是使用xib來製作webplayer 那可以直接把delegate給定

不然就是在.m
webplayer.delegate = self;
以及在.h 加上
@interface webPlayerViewController : UIViewController <UIWebViewDelegate> 


重點二
videoURL 是 http://www.youtube.com/embed/(youtubeID)
不是一般網址
https://www.youtube.com/watch?v=b1aHBlaC0de
也不是分享用的縮址
http://youtu.be/b1aHBlaC0de
舊版的嵌入網址也不適用
www.youtube.com/v/b1aHBlaC0de?version=3&amp;hl=zh_TW&amp;rel=0


最後重點
HTML 樣式的調整, 可以觀察一下NSString中一些跳脫字元的使用方式.
另外, 我們可以加上一個按鈕來關閉這頁.
UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithTitle:@"Done" 
         style:UIBarButtonItemStylePlain 
         target:self 
         action:@selector(onClickedDone:)];
self.navigationItem.rightBarButtonItem = doneButton;
[doneButton release];

- (void)onClickedDone:(id)sender {
    [webplayer release];
    webplayer = nil;
    //popViewController or dismissViewController
}


參考連結: MightyMeta
Sent from Evernote     

2013年11月21日 星期四

iOS7 UITableViewCell imageView offset issue

前言
iOS7的新界面風格確實給UX設計師跟工程師帶來不少困擾,
尤其是UITableView的layout變動讓我花了不少時間去調整。

問題
我在cell.imageView裡頭放的圖片位置跑掉了。

嘗試
// Not work 1.
[cell.imageViewsetFrame:CGRectOffset(cell.imageView.frame,-15,0)];

// Not work 2.
[tableViewsetContentOffset:CGPointMake(-15,0)];

// Not work 3.
if([selfrespondsToSelector:@selector(edgesForExtendedLayout)])
 self.edgesForExtendedLayout=UIRectEdgeNone;
 
// Not work 4.
if([mainTablerespondsToSelector:@selector(setSeparatorInset:)])
 [tableViewsetSeparatorInset:UIEdgeInsetsZero];
 
// Not work 5.
if([mainTablerespondsToSelector:@selector(setContentInset:)])
 [tableViewsetContentInset:UIEdgeInsetsZero];

實作
解決方法要點就是要實作custom cell.
第二點就是在custom cell的.m裡面 overwrite layoutSubviews 這個function.
因為只有iOS7會跑版, 所以多加個判斷式檢查.
#define IOS_SEVEN ([[UIDevice currentDevice].systemVersion floatValue] >= 7)

- (void)layoutSubviews{
 [superlayoutSubviews];
 if(IOS_SEVEN){
  self.imageView.frame=CGRectOffset(self.imageView.frame,-15,0);
 }
}

Sent from Evernote

2013年10月3日 星期四

[Android] 後悔了, 我想修改ActionBar的樣式

From Evernote:

[Android] 後悔了, 我想修改ActionBar的樣式

前言

一開始乖乖跟著別人的教學做出來的App大概像這樣,
不知道是那裡怪怪的, 總覺得有點生硬:
瞧瞧人家的就是比較fashion XD
對吼!就是看起來憨憨的ActionBar啦!

一開始專案建立沒選好,要怎麼辦呢?
還是可以反悔的,沒別招,手動改囉~



動手

事情是這樣發生的:
在AndroidMainifest.xml裡頭我節錄的最後一行,
說明了app theme是參考styles.xml裡面AppTheme的設定。

有頭緒了!而styles.xml藏在哪?
在專案/res/values,/res/values-v11,跟/res/values-v14底下都有stlyes.xml需要修改

然後重點來了AppBaseTheme 的parent值改成 android:Theme.XXX.NoTitleBar

至於有哪些Theme可以設定呢?
懶惰的方式除了參考xml的graphical layout設定後的效果


也可以看看官方網站是怎麼寫的~ Styles and Themes

參考資料:greenhandtobe开源中国

2013年9月15日 星期日

旋風式杜拜(Dubai)之行 - Day5

From Evernote:

旋風式杜拜(Dubai)之行 - Day5

前言(100/11/12)

每天起床迎接我的天氣都這麼好, 每棟大樓設計都不一樣, 賞盡各建築師的傑作, 讓人心情愉悅.


早上發現了附近的一家super market.
貨架上各式各樣的優格.


看起來應該是"舶來品"的水果


天氣熱嘛, 總是要吃點甜筒~


喔~然後網友有推薦 "必吃" 雪糕也讓我買到啦!!!! 還不錯喔 ^^b


觀光
飯店通往海邊必經的一座橋 (白天版)


橋上可以看到飯店後方的港灣, 果然停滿有錢人的遊艇


過了橋就是面海的飯店了, 飯店相連著好幾棟都由空橋串起. 


空橋下排班的計程車


飯店的大門側邊, 光線不錯, 順手就拍了


好~熱~鬧~阿!!!


不過癮? 再補一張 >////<


走累了, 找了一家比薩店


吃完中餐我就走回飯店, 也沒啥休息就跑到頂樓瞧瞧.
除了大馬路兩旁比較熱鬧, 再往內陸看去就是一片黃沙了~ 


飯店頂樓的泳池, 水靜靜的躺著沒有人打擾


冒險
回到房間想想明天就要搭機回台灣了.
心中總有些不甘(大誤) ,
看到梳妝台放著一本旅遊導覽, 一台沙漠中的吉普車後面一群駱駝映著日落餘暉.
在台灣作功課時網友也有介紹Desert Safari, 飯店櫃檯也都有agent可以馬上安排.
我心想下次什麼時候才有機會再來這麼神祕的國度.
心一橫, 衝到樓下找了飯店的agent幫我book行程. 
(幫我訂位的agent是位高挑金髮美女, 又很溫柔, 印象好極了>////<)

因為是散客, 所以與我同行的有兩位法國人跟一個俄羅斯妹妹.

駕駛是位身穿白袍的阿拉伯人,
一坐上車就可以聽到車子內濃濃的中東風音樂.
駕駛開得一副很悠哉的樣子, 不時跟副駕駛座的俄羅斯妹妹哈啦.
逗的她咯咯笑著~

駛離市區的高樓, 映入眼簾的是土黃色調的民宅.


中途的休息站, 可以看到附近已經幾乎是沙漠了


休息完後再度出發, 準備要去"衝沙"啦!!
駕駛越來越嗨~ 一邊開車一邊扭腰擺頭的XD
來到了沙丘邊, 駕駛下車把輪胎放掉一點氣.
然後回到駕駛座卻沒坐下來, 像特技表演一樣.
半個身體露在外邊, 一手持方向盤, 一腳踩油門, (車門開開的喔!)
阿~阿~阿~整車伴隨著尖叫聲就往下衝!!!

然後瘋狂的衝沙就開始了.
音樂聲幾乎開到最大!!
車窗外不時看到沙子噴上來.
駕駛最喜歡斜斜的從側邊開上沙丘, 然後在我們覺得車子快要翻了的時候,
猛然一轉方向盤, 突然往下衝, 搭配俄羅斯妹妹的尖叫聲.
我覺得駕駛超嗨的, 而且是越來越嗨XD

衝沙刺激歸刺激, 但是我不時撞到車頂,
而且大幅度的上下顛簸, 沒幾分鐘我就想吐了XD
幸好半路有下來看駱駝拍了幾張照片.
後來又有到山丘上跟其他車隊會合休息了第二次.
不過休息時間都很短, 保證沒有卡油水或虛應故事的啦!


沙漠中看著夕陽慢慢落下真的很美, 跟著大家倒數 5~4~3~2~1 然後一起讚嘆這幅美景!



晚上的BBQ buffet 大家自己找位子, 席地而坐, 看表演大啖美食!


晚上一直到8:30才離開營地, 回到飯店後只有我跟法國遊客下車.
這才知道俄羅斯妹妹要跟司機跑去"續攤" >/////< 我跟法國遊客相識而笑, 跟他們道別, 就各自回房休息了.
嗯~晚安了Dubai, 明天要搭機回家囉~

內容回應